package lox


class Scanner(
    private val source: String,
    private val tokens: ArrayList<Token> = ArrayList(),
    private var start: Int = 0,
    private var current: Int = 0,
    private var line: Int = 1,
) {

    companion object {
        val keywords: HashMap<String, TokenType> = HashMap<String, TokenType>().apply {
            this["and"] = TokenType.AND
            this["class"] = TokenType.CLASS
            this["else"] = TokenType.ELSE
            this["false"] = TokenType.FALSE
            this["for"] = TokenType.FOR
            this["fun"] = TokenType.FUN
            this["if"] = TokenType.IF
            this["nil"] = TokenType.NIL
            this["or"] = TokenType.OR
            this["print"] = TokenType.PRINT
            this["return"] = TokenType.RETURN
            this["super"] = TokenType.SUPER
            this["this"] = TokenType.THIS
            this["true"] = TokenType.TRUE
            this["var"] = TokenType.VAR
            this["while"] = TokenType.WHILE
        }
    }

    fun scanTokens(): ArrayList<Token> {
        while (!isAtEnd()) {
            start = current
            scanToken()
        }

        tokens.add(Token(TokenType.EOF, "", null, line))
        return tokens
    }

    private fun isAtEnd(): Boolean = current >= source.length

    private fun scanToken() {
        when (val ch = advance()) {
            '(' -> addToken(TokenType.LEFT_PAREN)
            ')' -> addToken(TokenType.RIGHT_PAREN)
            '{' -> addToken(TokenType.LEFT_BRACE)
            '}' -> addToken(TokenType.RIGHT_BRACE)
            ',' -> addToken(TokenType.COMMA)
            '.' -> addToken(TokenType.DOT)
            '-' -> addToken(TokenType.MINUS)
            '+' -> addToken(TokenType.PLUS)
            ';' -> addToken(TokenType.SEMICOLON)
            '*' -> addToken(TokenType.STAR)

            '!' -> addToken(if (match('=')) TokenType.BANG_EQUAL else TokenType.BANG)
            '=' -> addToken(if (match('=')) TokenType.EQUAL_EQUAL else TokenType.EQUAL)
            '<' -> addToken(if (match('=')) TokenType.LESS_EQUAL else TokenType.LESS)
            '>' -> addToken(if (match('=')) TokenType.GREATER_EQUAL else TokenType.GREATER)

            '/' -> if (match('/')) {
                while (peek() != '\n' && !isAtEnd()) advance()
            } else {
                addToken(TokenType.SLASH)
            }

            ' ', '\r', '\t' -> {}

            '\n' -> line++

            '"' -> string()

            else -> if (ch.isDigit()) {
                number()
            } else if (ch.isLetter()) {
                identifier()
            } else {
                Lox.error(line, "Unexpected character '$ch'")
            }
        }
    }

    private fun identifier() {
        while (peek().run { this.isLetter() || this.isDigit() }) advance()

        source.substring(start, current).run {
            addToken(keywords[this] ?: TokenType.IDENTIFIER)
        }
    }

    private fun advance(): Char {
        current++
        return source[current - 1]
    }

    private fun peek(): Char {
        if (isAtEnd()) return '\u0000'
        return source[current]
    }

    private fun peekNext(): Char {
        if (current + 1 >= source.length) return '\u0000'
        return source[current + 1]
    }

    private fun match(expected: Char): Boolean {
        if (isAtEnd()) return false
        if (source[current] != expected) return false

        current++
        return true
    }

    private fun addToken(type: TokenType, literal: Any? = null) {
        val text = source.substring(start, current)
        tokens.add(Token(type, text, literal, line))
    }

    private fun string() {
        while (peek() != '"' && !isAtEnd()) {
            if (peek() == '\n') line++
            advance()
        }

        if (isAtEnd()) {
            Lox.error(line, "Unterminated string")
            return
        }

        // closing "
        advance()

        source.substring(start + 1, current - 1).apply {
            addToken(TokenType.STRING, this)
        }
    }


    private fun number() {
        while (peek().isDigit()) advance()

        // 11.22
        // disallow .1 1.
        if (peek() == '.' && peekNext().isDigit()) {
            // .
            advance()

            while (peek().isDigit()) advance()
        }

        source.substring(start, current).toDouble().run { addToken(TokenType.NUMBER, this) }

    }
}